iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0

Type Narrowing 是一種概念,指在程式碼執行過程中,根據特定條件,將變數的型別從一個較廣泛的型別縮小為較特定的型別,以提供更精確的型別資訊,這種功能有助於提高程式碼的可讀性和可維護性,同時確保運行時更安全。( 有點類似使用 unknown 要先型別檢查 )。

優點:

  • 提高程式碼可讀性 型別縮小使我們可以在程式碼中提供更多上下文信息,幫助其他開發者更容易理解。

  • 增加型別安全性 型別縮小可以減少運行時發生錯誤的可能性,因為編譯器可以提前檢測到型別不相容的操作。

型別縮小的方法

  • typeof

typeof 會回傳的類型有:string、number、boolean、object、function、undefined、bigint、symbol。

看以下範例:

const printValue = (value: string | number): void => {
  const print =
    typeof value === "string" ? `${value}好棒棒!` : value.toFixed(2);
  console.log(print);
};

printValue("威爾豬"); // 輸出: 威爾豬好棒棒!
printValue(3); // 輸出: 3.00

在上面的範例中,typeof value === "string" 縮小了 value 變數的型別,因此我們可以安全地使用樣板字面值。

不過要特別注意 null,它的 typeof 值和陣列一樣是 object:

const printValue = (value: string | string[] | null): void => {
  if (typeof value === "object") {
    for (const item of value) {
      console.log(item);
    }
  } else if (typeof value === "string") {
    console.log(value);
  } else {
    console.log("這是 null");
  }
};

printValue("威爾豬"); // 輸出: 威爾豬
printValue(null); // ❌ 錯誤,null 不能跑迴圈

我們可以修改上面的範例為:

const printValue = (value: string | string[] | null) {
  if (value && typeof value === "object") {
    for (const item of value) {
      console.log(item);
    }
  } else if (typeof value === "string") {
    console.log(value);
  }else {
    console.log("這是 null");
  }
}

printValue("威爾豬"); // 輸出: 威爾豬
printValue(null); // ⭕️ 輸出: 這是 null
  • in

interface ICircle {
  radius: number;
}

interface IRect {
  width: number;
  height: number;
}

type TShape = ICircle | IRect;

const getArea = (shape: TShape): void => {
  let shapeText: string = "";
  let area = 0;

  if ("radius" in shape) {
    shapeText = "圓形";
    area = Math.round(Math.PI * shape.radius ** 2);
  } else {
    shapeText = "方形";
    area = shape.width * shape.height;
  }

  console.log(`${shapeText}面積為 ${area}`);
};

const circle: ICircle = { radius: 10 };
const rect: IRect = { width: 2, height: 3 };

getArea(circle); // 輸出: 圓形面積為 314
getArea(rect); // 輸出: 方形面積為 6

在上面的範例中,"radius" in shape 縮小了 shape 的接口類型,所以我們可以明確的知道現在的形狀及面積。

  • instanceof

instanceof 用来檢查一個值是否是另一個值的「實體 / 實例 ( Instance )」,也就是檢查建構函式的 prototype 屬性是否存在於某個實體的原型鏈上。

看以下範例:

class Animal {}
class Cat extends Animal {}

const makeSound = (animal: Animal): void => {
  const sound = animal instanceof Cat ? "喵喵喵~" : "我不知道叫聲";
  console.log(sound);
};

const myCat: Cat = new Cat();
makeSound(myCat); // 輸出: 喵喵喵~
  • 非空斷言 (!)

非空斷言是 TypeScript 專屬的語法,這種表示法使用 ! 放在變數名稱或屬性後面。我們可以使用非空斷言來縮小型別,表示 這個值的型別不會是 null 或 undefined,並且希望 TypeScript 忽略可能的空值錯誤檢查。

const greet = (name?: string | null): void => {
  console.log(`哈囉,${name!.trim()}!`);
};

greet("     威爾豬"); // 輸出: 哈囉,威爾豬!
greet(); // ❌ 執行錯誤

上面範例我們將 name 參數使用非空斷言,表示我們跟 typescript 說這個值不會是 null 或 undefined,請忽略空值的檢查。

非空斷言要小心使用,因為實際上變數為 null 或 undefined 時,在執行就會引發錯誤。

  • 型別謂詞 ( Type Predicates )

Type predicates 是使用 自定義帶有特殊的返回型別的函數

看以下範例:

interface ICat {
  run(): void;
}

interface IBird {
  swim(): void;
}

type TPet = ICat | IBird;

// 使用 type predicates 來確定 pet 是否是 ICat 接口
function isCat(pet: TPet): pet is ICat {
  return "run" in pet;
}

const myPet = (pet: TPet): void => {
  isCat(pet) ? pet.run() : pet.swim();
};

const myCat: ICat = {
  run() {
    console.log("我的貓在跑");
  },
};

const myBird: IBird = {
  swim() {
    console.log("我的魚在游");
  },
};

myPet(myCat); // 輸出: 我的貓在跑
myPet(myBird); // 輸出: 我的魚在游
  • 區分聯合型別 ( Discriminated Unions )

我們使用具有相同名稱的屬性來區分不同的型別,這些屬性通常被稱為 標記,每個型別都擁有唯一的標記值,因此 TypeScript 可以根據標記值來確定變數的正確型別。

範例如下:

interface ICircle {
  kind: "circle";
  radius: number;
}

interface IRect {
  kind: "rect";
  width: number;
  height: number;
}

type TShape = ICircle | IRect;

function getArea(shape: TShape): number {
  let area = 0;

  // 使用 Discriminated unions 來判斷型別
  switch (shape.kind) {
    case "circle":
      area = Math.round(Math.PI * shape.radius ** 2);
      console.log(`圓形面積為 ${area}`);
      return;
    case "rect":
      area = shape.width * shape.height;
      console.log(`方形面積為 ${area}`);
      return;
    default:
      throw new Error("沒有這個圖形");
  }
}

const myCircle: ICircle = {
  kind: "circle",
  radius: 10,
};

const myRect: IRect = {
  kind: "rect",
  width: 2,
  height: 3,
};

getArea(myCircle); // 輸出: 圓形面積為 314
getArea(myRect); // 輸出: 方形面積為 6

其實有時候我們不知不覺都在使用 Type Narrowing,它可以透過使用 typeof、instanceof、in 等條件限制來縮小變數的類型範圍,使 TypeScript 和其他開發者可以更好地理解變數的實際類型,並適當地進行型別檢查和推斷,我們可以在程式碼中充分利用這一功能,以提高開發效率並減少錯誤。


上一篇
模組 & 命名空間 ( Modules & Namespaces )
下一篇
高級型別
系列文
用不到 30 天學會基本 TypeScript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言